ECE 5725 Spring 2023
Juliet DeNapoli (jfd232), Changyuan Lin (cl859), Owen Louis (ocl6)
This project is a physical rendition of the classic Minesweeper video game, using a hardware game board where squares can be uncovered by pressing them directly. There is an 8x8 grid, with each grid square illuminated from within by a colored RGB LED to display the nearby mine count, undiscovered status, flag, or location of mine when the game is lost. The player is able to push the grid squares down to explore that space, with other open squares automatically being revealed like how auto-exploration works in the traditional Minesweeper game. The Raspberry Pi and all hardware are enclosed in a custom 3D printed chassis, and the PiTFT displays a leaderboard with previous best times, filtered by difficulty.
The game was controlled using a Python program, with separate files for the leaderboard and saved game replay, containing the winning scores of the game and moves, seed, and difficulty for the most recently played game. Our game begins by randomly placing the mines throughout the board. The number of mines depends on the difficulty selected using the PiTFT buttons. 10 mines are placed at the default "easy" difficulty, but the player can choose to play medium (15 mines) or hard (20 mines). During initialization, the game computed the colors of all squares by looping through them and looking at the number of mines in adjacent grid squares. This saved computation time during gameplay, since the colors are known in advance but only revealed when the grid square is uncovered.
Once the board was generated, the game starts with all the squares lit up in white, to indicate everything as unrevealed. There were two types of presses, a long and short press. These were determined by recording the press time when buttons were pushed down in a dictionary, and calculating the duration when the button was released. A short press would reveal an unrevealed square, and depending on the sum of mines around that unrevealed square, the square would change color to indicate how many mines were around it. For one mine, it was blue, for two it was green, for three it was red, for four it was purple, and for five it was yellow. The square turns off if there are no adjacent mines. If the unrevealed square turned out to be a mine, revealing it would flash all the mines on the board red to indicate that the player lost, while also pausing the time and disabling any further interaction other than resetting the game.
However, if the player would have revealed a mine on the first try of the whole game, our board regenerated to a different board until it could be guaranteed that the given square was clear. This prevented the player from instantly losing, and thus made our game more enjoyable. We also implemented auto-exploration features like the original game, where if the player revealed a square with no adjacent mines, the adjacent squares were also progressively revealed until a colored number is revealed. This was done with breadth-first search, which provided a cool effect of the clear areas expanding from the touch point until they hit colored grid spaces.
A longer press flagged a square, and was indicated by a flashing white light. Once a square is flagged, it cannot be uncovered by a short press until it is unflagged by another long press. It is useful for the player to keep track of where all the mines are while playing. Flagging a square also decreased the remaining mine count in the segmented display on the top left of the system. If the player flags too many mines, this number turns negative, to indicate that some of the flagged squares the player thought were mines are not actually mines. The player cannot win until all safe squares are revealed.
The numeric display in the top right corner of our system displayed the time. If the player uncovered all the squares that didn't contain mines, they won, and their time was added to our leaderboard file along with the difficulty and date achieved. Our PiTFT displayed these best times, filtering through the leaderboard file to only display times from the current difficulty, sorted by lowest times. The first, second, and third place scores were displayed as gold, silver, and bronze, respectively. The leaderboard file persists across reboots and the Python program automatically starts when the system is turned on to provide for an embedded system.
After winning or losing a game, or if the player wanted to play a new game at any point, they could quick reset the game at the current difficulty using the big red button in the lower right corner. Alternatively, they could also reset the game with the difficulty buttons in case they wanted to play at a different mode. Resetting the game regenerated the board state with the corresponding number of mines, and reset the timer and flag states.
A new feature added after our final project demo, in preparation for static display, was the replay mode. This addition let the player record their played games into a log of actions taken, which was saved to a file, along with the game difficulty and game seed so the same game board can be regenerated later. Running the same program, the game log can be supplied as an additional argument, which disables interaction and has the Pi autonomously generate the game board and play it over and over again, with the same delays between button presses as the original human player. In replay mode, the new games played by the Pi were not recorded to the leaderboard, which still shows the regular leaderboard content.
In order to display the typical grid of Minesweeper, we used four 4x4 button and LED combination circuit boards from Adafruit. These boards can display any RGB color, and the physical buttons are made of durable and transparent silicone. The boards are communicated with via I2C. Each board has five unique address changing soldering points that allowed us to use four in combination. This extra capacity would also allow us to further expand this project given a larger budget. These boards also have an interrupt pin that is used to trigger an interrupt service routine on the Raspberry Pi to ensure a rapid response to any button press.
Displaying the timer and mine counter was handled by two sets of four digit 14-segment display boards from Adafruit. These boards also communicated over I2C, and they had two address changing solder points. These displays were wired directly into the same I2C wires as the LED boards resulting in only five wires needing to be connected to the Raspberry Pi: 3V, GND, SDA, SCL, and one interrupt pin from the LED boards.
The arcade style button used a typical button microswitch given to us by Professor Skovira. This switch was wired to be a default low button in order to use the lowest amount of current.
The non-electronic hardware design stemmed largely from our initial concept drawing seen in Figure 1. This initial design sketch was made using rough measurements of all the components used, but it was merely an initial design idea. As we progressed, the design was segmented into distinct components due to the limitations of 3D printing bed sizes, and the design was also brought into three dimensions to resemble an arcade machine.
The first piece printed was the main body holding the four LED button grids and the two sets of 14-segment displays on the top. This piece took five drafts to get to be the perfect shape as the buttons needed just enough room to be pressed while also being structurally supportive. This component was also designed to house the 14-segment displays shown at the top of Figure 1. These displays needed to be perfectly flush with the front of the display while leaving room behind for their control boards and breakout pins. We also included two cylindrical mounting points components that can be seen in the bottom of Figure 2 that the piece holding the PiTFT and arcade style button would later mount to.
In order to retain the 4x4 LED matrix circuit boards together and in place, we had to design a custom solution. The circuit boards needed to be supported from the rear, but there was limited area from which they could be due to various resistors and capacitor placements. In order to fully support the LED circuit boards enough to withstand constant use from players, we used an I-beam style support that pressed against every corner of the circuit boards. We left gaps in the supports along each side of the circuit boards so that wires could be run from the 14-segment displays down to the Raspberry Pi.. We then used a small tab based alignment method to ensure that the support structure was in the correct position while gluing which can be seen in Figure 3. We also selected the I-beam style support due to its lower plastic volume than a more blocky style.
The bottommost part of the hardware design housed the Raspberry Pi, PiTFT, and arcade style button. The PiTFT was difficult to constrain in all directions for several reasons. The first was that the premade mounting holes are so small that 3D printed parts lack much strength at that size. The second is that the Raspberry Pi and PiTFT display are not exactly parallel, and this creates some difficult dimensions to measure. In order to get around the first issue we printed many drafts to find the exact maximum radius we could get away with, and we also used the port housing portion of the Raspberry Pi as an additional constraint. This cutout can be seen in the bottom right of Figure 4. In order to resolve the second issue, we used rubber foam sandwiched between the Raspberry Pi and the back of the housing to press the assembly against the front and thus constrain it depth-wise. We also had to make some small design modifications to accommodate some pins and pieces of solder on the PiTFT that can be seen in Figure 4. At the top of the figure below you can also see the cutout for the wiring between the two main bodies.
All remaining pieces of the final assembly were aesthetic only. This includes the “Pi Sweeper” name plate seen at the top of Figure 5 and the rear piece of the main body. We also printed our names as the structural supports of the two legs, and this can be partially seen below as well.
The arcade style button needed to be redesigned to fit within the tight depth constraints of its housing. The design was adapted from an open source design by Thingiverse user Wattage2308, and we only modified the inner radius of the outermost part to facilitate a more smooth button pressing motion. See the references section for the credited design and download link.
When we first received our parts, none of the NeoTrellis LED keypads were connected yet, so we began by testing our board generation on one 4x4 NeoTrellis board. This was a matter of creating a grid as our gameboard, randomly placing a few mines within this grid, and computing the surrounding mines for all the clear squares. This initial setup can be seen in Figure 6. When it came time to add more NeoTrellis boards, it was just a matter of changing the x and y dimensions of our game board, as well as adding the new NeoTrellis board to our array of boards with the right I2C addresses.
After this first draft, we were able to connect all the NeoTrellis boards, as well as connect the numeric displays. This can be seen in Figure 7. From here, we implemented board clearing, flagging, as well as the flag and time display on the numeric LEDs.
With all of this done, we added the bottom component of our system, which had the PiTFT and the reset button. We implemented how the reset button would implement the game, and how each of the different difficulty buttons on the PiTFT would reset the game with its respective. From there, the last part to complete was the leaderboard to be displayed on the PiTFT. After the final demo we also added the replay mode and tested it.
The final result of our project was full completion of our objective, and it ended up being a very fun game to play for everyone in the lab, so much that we had people coming over to play it. Every component that we purchased worked without problems, and integration was smooth due to the ease of I2C on the Raspberry Pi. We ended up slightly changing our chassis design as we went to create a better user experience when playing the game. The game looked and played as well as we could have hoped despite limitations of only having LEDs as the display.
In the future, we could expand upon our game to create an even bigger board. Luckily, with the way we developed our code to be modular, adding more boards wouldn't be difficult. As of right now, our hard difficulty is just a little too difficult, but that is only because it is on an 8x8 grid. In other versions of Minesweeper, different difficulties come with different sized boards.
cl859@cornell.edu
jfd232@cornell.edu
ocl6@cornell.edu
import time
import board
import random
import RPi.GPIO as GPIO
import pygame
import os
import sys
from adafruit_neotrellis.neotrellis import NeoTrellis
from adafruit_neotrellis.multitrellis import MultiTrellis
from adafruit_ht16k33.segments import Seg14x4
from bisect import bisect
from collections import deque
from datetime import datetime
from pygame.locals import * # For mouse variables
# Size of the board
X_DIM = 8
Y_DIM = 8
# Colors
OFF = BLACK = (0, 0, 0)
RED = (255, 0, 0)
DARKRED = (127, 0, 0)
ORANGE = (255, 150, 0)
YELLOW = (255, 255, 0)
GREEN = (0, 255, 0)
CYAN = (0, 255, 255)
BLUE = (0, 0, 255)
PURPLE = (180, 0, 255)
WHITE = (255, 255, 255)
LESSWHITE = (40, 40, 40)
SILVER = (150, 150, 150)
# Tile colors, index by number of mines
TILE_COLORS = [OFF, BLUE, GREEN, RED, PURPLE, ORANGE, CYAN]
# Mines per difficulty (easy, medium, hard)
DIFFICULTY_MINES = [10, 15, 20]
# Leaderboard file
LEADERBOARD_FILE = "leaderboard.csv" # Relative to working directory
# Actions log file
ACTIONS_LOG_FILE = "game.log"
# Declare the game variable
game = None
start_time = None
difficulty = 0
# In-memory leaderboard will be sorted by game duration
leaderboard = []
# Record actions
actions_start_time = None
actions_list = []
# Replay mode (disables adding to leaderboard, buttons)
replay_mode = False
replay_difficulty = None
replay_seed = None
replay_list = []
replay_index = 0
replay_start_time = None
# Enable replay mode if argument given
if len(sys.argv) == 2:
replay_mode = True
with open(sys.argv[1], "r") as log:
for line in log:
line = line.strip()
if line:
if line.startswith("difficulty"):
replay_difficulty = int(line.split(" ")[1])
elif line.startswith("seed"):
replay_seed = float(line.split(" ")[1])
else:
columns = line.split(",")
replay_list.append((int(columns[0]), int(columns[1]), int(columns[2]), float(columns[3])))
code_run = True
def quit_callback(channel):
global code_run
code_run = False
class Tile:
def __init__(self, x, y):
self.number = 0
self.flagged = False
self.revealed = False
self.x = x
self.y = y
class Minesweeper:
def __init__(self, xdim, ydim, nmines):
self.seed = time.time() # Seed with system time and remember it
if replay_mode:
self.seed = replay_seed
print("Generating with seed", self.seed)
random.seed(self.seed)
self.xdim = xdim
self.ydim = ydim
self.nmines = nmines
self.board = [[Tile(x, y) for x in range(xdim)] for y in range(ydim)]
self.place_mines(nmines)
self.compute_numbers()
self.lost = False
self.stop_elapsed = None
def place_mines(self, nmines):
if nmines > self.xdim * self.ydim:
print("Too many mines!!")
imine = 0
while imine < nmines:
x = random.randrange(0, self.xdim)
y = random.randrange(0, self.ydim)
if self.board[y][x].number != -1:
self.board[y][x].number = -1
imine += 1
def compute_numbers(self):
for y in range(self.ydim):
for x in range(self.xdim):
if self.board[y][x].number == -1:
continue
nearby = 0
for dy in range(-1, 2):
for dx in range(-1, 2):
if (
0 <= y + dy < self.ydim
and 0 <= x + dx < self.xdim
and self.board[y + dy][x + dx].number == -1
):
nearby += 1
self.board[y][x].number = nearby
def debug_print(self):
print("-" * self.xdim)
for y in range(self.ydim):
for x in range(self.xdim):
if self.board[y][x].number == -1:
print("X", end="")
elif self.board[y][x].number == 0:
print(" ", end="")
else:
print(self.board[y][x].number, end="")
print()
print("-" * self.xdim)
def reveal_square(self, x, y):
tiles = deque([self.board[y][x]])
seen = deque([self.board[y][x]])
while tiles:
tile = tiles.popleft()
# If touched a mine
if tile.number == -1:
self.lost = True
return
# Else touched a safe square
else:
tile.revealed = True
trellis.color(tile.x, tile.y, TILE_COLORS[tile.number])
# Add nearby tiles to reveal if it was a 0
if tile.number == 0:
for dy in range(-1, 2):
for dx in range(-1, 2):
if (
0 <= tile.y + dy < self.ydim
and 0 <= tile.x + dx < self.xdim
and self.board[tile.y + dy][tile.x + dx] not in seen
and not self.board[tile.y + dy][tile.x + dx].flagged
):
tiles.append(self.board[tile.y + dy][tile.x + dx])
seen.append(tile)
def is_loss(self):
return self.lost
def is_win(self):
clear = True
for y in range(self.ydim):
for x in range(self.xdim):
if not self.board[y][x].revealed and self.board[y][x].number != -1:
clear = False
return clear
def color_flagged(self, color):
for y in range(self.ydim):
for x in range(self.xdim):
if self.board[y][x].flagged:
trellis.color(x, y, color)
def color_mines(self, color):
for y in range(self.ydim):
for x in range(self.xdim):
if self.board[y][x].number == -1:
trellis.color(x, y, color)
def num_flagged(self):
num = 0
for y in range(self.ydim):
for x in range(self.xdim):
if self.board[y][x].flagged:
num += 1
return num
def reset_game(difficulty):
global game
global start_time
game = Minesweeper(X_DIM, Y_DIM, DIFFICULTY_MINES[difficulty])
game.debug_print()
start_time = None
segment_mine.fill(0)
segment_mine.print(game.nmines - game.num_flagged())
segment_time.fill(0)
segment_time.print(0)
for y in range(Y_DIM):
for x in range(X_DIM):
# color white by default
trellis.color(x, y, WHITE)
def load_leaderboard():
try:
with open(LEADERBOARD_FILE, "r") as leaderboard_file:
for line in leaderboard_file:
line = line.strip()
if line:
columns = line.split(",")
leaderboard.append(
(
datetime.fromisoformat(columns[0]),
int(columns[1]),
float(columns[2]),
)
)
leaderboard.sort(key=lambda tup: tup[2])
except FileNotFoundError:
print("No leaderboard file found", LEADERBOARD_FILE)
def append_leaderboard(timestamp, difficulty, gametime):
with open(LEADERBOARD_FILE, "a") as leaderboard_file:
leaderboard_file.write(f"{timestamp},{difficulty},{gametime}\n")
ranking = bisect([tup[2] for tup in leaderboard], gametime)
leaderboard.insert(ranking, (timestamp, difficulty, gametime))
return ranking
# Hardware setup
GPIO.setmode(GPIO.BCM)
GPIO.setup(26, GPIO.IN, pull_up_down=GPIO.PUD_UP)
if not replay_mode:
GPIO.add_event_detect(
26, GPIO.FALLING, callback=lambda channel: reset_game(difficulty), bouncetime=300
)
GPIO.setup(27, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(27, GPIO.FALLING, callback=quit_callback, bouncetime=300)
GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(22, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP)
# Create the i2c object for the trellis
i2c_bus = board.I2C() # uses board.SCL and board.SDA
# i2c_bus = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller
# Create the trellis
trelli = [
[NeoTrellis(i2c_bus, False, addr=0x2E), NeoTrellis(i2c_bus, False, addr=0x2F)],
[NeoTrellis(i2c_bus, False, addr=0x30), NeoTrellis(i2c_bus, False, addr=0x31)],
]
trellis = MultiTrellis(trelli)
# Set the brightness value (0 to 1.0)
trellis.brightness = 0.5
# Create a 14 segment display
segment_mine = Seg14x4(i2c_bus, address=0x71)
segment_time = Seg14x4(i2c_bus, address=0x70)
# Button held status
buttons_held = {}
# Flagged flash time
flash_time = time.time()
flash_on = False
# Variable to only refresh timer when needed
last_shown_time = 0
# Record actions
def record_action(xcoord, ycoord, edge):
global actions_start_time
global actions_list
if not game.stop_elapsed:
if not actions_start_time:
actions_start_time = time.time()
actions_list = [(xcoord, ycoord, edge, 0)]
else:
actions_list.append((xcoord, ycoord, edge, time.time() - actions_start_time))
# Finish recording of a game
def record_end():
global actions_start_time
global actions_list
print(actions_list)
with open(ACTIONS_LOG_FILE, "w") as log_file:
log_file.write(f"difficulty {difficulty}\n")
log_file.write(f"seed {game.seed}\n")
for action in actions_list:
log_file.write(f"{action[0]},{action[1]},{action[2]},{action[3]}\n")
actions_start_time = None
actions_list = []
# Replay simulates button presses
def replay_update():
global replay_index
global replay_start_time
if not replay_mode:
print("Replay mode not enabled!")
return
if not replay_start_time:
replay_start_time = time.time()
replay_elapsed = time.time() - replay_start_time
while replay_index < len(replay_list) and replay_list[replay_index][3] <= replay_elapsed:
xcoord, ycoord, edge, _ = replay_list[replay_index]
press(xcoord, ycoord, edge)
replay_index += 1
# If at the end, wait for a while and then reset
if replay_index == len(replay_list) and replay_list[replay_index - 1][3] + 5 <= replay_elapsed:
reset_game(replay_difficulty)
replay_index = 0
replay_start_time = None
# Callback for button events
def press(xcoord, ycoord, edge):
global start_time
global game
if not replay_mode:
record_action(xcoord, ycoord, edge)
if edge == NeoTrellis.EDGE_RISING:
# When pressing down, save the current time
buttons_held[(xcoord, ycoord)] = time.time()
elif edge == NeoTrellis.EDGE_FALLING:
# Safety check that the key exists -- sometimes this causes an error
if (xcoord, ycoord) not in buttons_held:
print(f"Problem with {xcoord}, {ycoord}, skipping")
return
# Get how long the button was pressed
duration = time.time() - buttons_held[(xcoord, ycoord)]
del buttons_held[(xcoord, ycoord)]
# On first press make sure it's not a mine (regen board)
while not start_time and game.board[ycoord][xcoord].number == -1:
game = Minesweeper(X_DIM, Y_DIM, DIFFICULTY_MINES[difficulty])
# Only process presses if game hasn't ended and not revealed
if not game.board[ycoord][xcoord].revealed and not game.stop_elapsed:
# Flag/unflag if held long enough
if duration >= 0.3:
if game.board[ycoord][xcoord].flagged:
print(f"Unflag {xcoord}, {ycoord}")
game.board[ycoord][xcoord].flagged = False
trellis.color(xcoord, ycoord, WHITE)
else:
print(f"Flag {xcoord}, {ycoord}")
game.board[ycoord][xcoord].flagged = True
# Update segment display with number of mines remaining
segment_mine.fill(0)
segment_mine.print(game.nmines - game.num_flagged())
# Reveal mine and check game status on regular press
elif not game.board[ycoord][xcoord].flagged:
print(f"Reveal {xcoord}, {ycoord}")
game.reveal_square(xcoord, ycoord)
if not start_time:
start_time = time.time()
if game.is_loss():
print("YOU LOSE")
game.stop_elapsed = time.time() - start_time
segment_time.fill(0)
segment_time.print(int(game.stop_elapsed))
segment_mine.fill(0)
segment_mine.print("LOSE")
if not replay_mode:
record_end()
elif game.is_win():
print("YOU WIN")
game.stop_elapsed = time.time() - start_time
if not replay_mode:
ranking = append_leaderboard(
datetime.now(), difficulty, game.stop_elapsed
)
print("Ranking", ranking)
draw_home()
segment_time.fill(0)
segment_time.print(int(game.stop_elapsed))
segment_mine.fill(0)
segment_mine.print("WIN")
if not replay_mode:
record_end()
# Set up key callbacks
if not replay_mode:
for y in range(Y_DIM):
for x in range(X_DIM):
# activate rising edge events on all keys
trellis.activate_key(x, y, NeoTrellis.EDGE_RISING)
# activate falling edge events on all keys
trellis.activate_key(x, y, NeoTrellis.EDGE_FALLING)
# set all keys to trigger the blink callback
trellis.set_callback(x, y, press)
# color white by default
trellis.color(x, y, RED)
# Handler for difficulty button presses (callbacks not all working)
def handle_difficulty_buttons():
global difficulty
if not GPIO.input(17):
difficulty = 0
draw_home()
reset_game(difficulty)
elif not GPIO.input(22):
difficulty = 1
draw_home()
reset_game(difficulty)
elif not GPIO.input(23):
difficulty = 2
draw_home()
reset_game(difficulty)
# PyGame setup
os.putenv("SDL_VIDEODRIVER", "fbcon") # Display on piTFT
os.putenv("SDL_FBDEV", "/dev/fb0")
# os.putenv('SDL_MOUSEDRV', 'TSLIB') # Track mouse clicks on piTFT
# os.putenv('SDL_MOUSEDEV', '/dev/input/touchscreen')
pygame.init()
pygame.mouse.set_visible(False)
screen = pygame.display.set_mode((320, 240))
screen.fill(BLACK)
font_big = pygame.font.Font(None, 32)
font_small = pygame.font.Font(None, 24)
def draw_add_leaderboard():
date_header = font_big.render("Date", True, WHITE)
time_header = font_big.render("Time", True, WHITE)
screen.blit(date_header, (30, 10))
screen.blit(time_header, (180, 10))
vertical_spacing = 24
leaderboard_difficulty = [tup for tup in leaderboard if tup[1] == difficulty]
for i, (date, _, time) in enumerate(leaderboard_difficulty):
if i == 0:
color = YELLOW
elif i == 1:
color = SILVER
elif i == 2:
color = ORANGE
else:
color = WHITE
rank_text = font_small.render(str(i), True, color)
date_text = font_small.render(date.isoformat(" ", "minutes"), True, color)
time_text = font_small.render(str(round(time, 3)), True, color)
screen.blit(rank_text, (10, 40 + vertical_spacing * i))
screen.blit(date_text, (30, 40 + vertical_spacing * i))
screen.blit(time_text, (180, 40 + vertical_spacing * i))
def draw_add_difficulties():
difficulties = [("EASY", BLUE), ("MED", GREEN), ("HARD", RED)]
for i, (name, color) in enumerate(difficulties):
selected = i == difficulty
rect = pygame.Rect(
280 if selected else 290, 20 + 60 * i, 40 if selected else 30, 60
)
screen.fill(color, rect)
text = pygame.transform.rotate(font_small.render(name, True, WHITE), 90)
screen.blit(text, text.get_rect(center=rect.center))
def draw_home():
screen.fill(BLACK)
draw_add_leaderboard()
draw_add_difficulties()
pygame.display.flip()
# Initialize the leaderboard
load_leaderboard()
print(leaderboard)
# Reset the game for first run
reset_game(0 if not replay_mode else replay_difficulty) # Easy by default
draw_home()
# Main game loop
while code_run:
# call the sync function call any triggered callbacks
trellis.sync()
# Flash flagged squares
game.color_flagged(WHITE if flash_on else LESSWHITE)
# Flash mines if game lost
if game.is_loss():
game.color_mines(WHITE if flash_on else RED)
# Update flash state
if time.time() - flash_time >= 0.5:
flash_on = not flash_on
flash_time = time.time()
# Update timer display
if start_time and not game.stop_elapsed:
elapsed = int(time.time() - start_time)
if elapsed != last_shown_time:
segment_time.fill(0)
segment_time.print(elapsed)
last_shown_time = elapsed
# Handle playback if replay mode
if replay_mode:
replay_update()
# Otherwise check polling buttons as normal
else:
# Check for difficulty button presses
handle_difficulty_buttons()
# the trellis can only be read every 17 millisecons or so
time.sleep(0.02)
GPIO.cleanup()